/**
 * Copyright 2008 Mathias Doenitz, http://lis.to/
 *
 * This file is part of the lis.to java desktop client. The lis.to java desktop client is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * The lis.to java desktop client is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with the lis.to java desktop client.
 * If not, see http://www.gnu.org/licenses/
 */

package listo.client;

import com.google.common.collect.Lists;
import com.google.inject.Inject;
import listo.utils.exceptions.IllegalFormattingException;
import listo.utils.guice.CalendarDayScoped;
import listo.utils.types.Cal;
import listo.utils.types.DateTime;
import org.apache.commons.lang.StringUtils;

import java.text.DateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@CalendarDayScoped
public class Formatter {

    private static final Pattern PATTERN_ON_TIME = Pattern.compile(
            "\\A(?:on.*)|(?:0)", Pattern.CASE_INSENSITIVE);
    private static final Pattern PATTERN_MIN_BEFORE = Pattern.compile(
            "\\A(?:-(\\d+))|(?:(\\d+).*before)", Pattern.CASE_INSENSITIVE);
    private static final Pattern PATTERN_MIN_AFTER = Pattern.compile(
            "\\A(?:\\+(\\d+))|(?:(\\d+).*after)", Pattern.CASE_INSENSITIVE);
    private static final Pattern PATTERN_MIN_AGO = Pattern.compile(
            "\\A(?:\\-\\-(\\d+))|(?:(\\d+).*ago)", Pattern.CASE_INSENSITIVE);
    private static final Pattern PATTERN_IN_MIN = Pattern.compile(
            "\\A(?:\\+\\+(\\d+))|(?:in\\s(\\d+).*)", Pattern.CASE_INSENSITIVE);

    private final Cal today = new Cal().beginningOfDay();
    private final DateFormat dateFormat;
    private final DateFormat shortDateFormat;
    private final String[] weekdays;
    private final String[] shortWeekdays;
    private final List<String> next9Text;
    private final List<String> next9TextLowered = Lists.newArrayList();
    private final List<DateTime> next9Date = Lists.newArrayList();

    @Inject
    public Formatter(Lang lang) {
        dateFormat = lang.getDateFormat();
        shortDateFormat = lang.getShortDateFormat();
        next9Text = Lists.newArrayList(lang.get("days.today"), lang.get("days.tomorrow"));
        weekdays = StringUtils.split(lang.get("days.weekdays"), ',');
        shortWeekdays = StringUtils.split(lang.get("days.shortWeekdays"), ',');

        int today = this.today.get(Calendar.DAY_OF_WEEK);
        int todayIx = toDaysIndex(today);
        int todayPlus1 = this.today.add(Calendar.DATE, 1).get(Calendar.DAY_OF_WEEK);
        int todayPlus1Ix = toDaysIndex(todayPlus1);
        int todayPlus2 = this.today.add(Calendar.DATE, 2).get(Calendar.DAY_OF_WEEK);
        int todayPlus2Ix = toDaysIndex(todayPlus2);

        next9Text.addAll(Arrays.asList(weekdays).subList(todayPlus2Ix, weekdays.length));
        next9Text.addAll(Arrays.asList(weekdays).subList(0, todayIx));
        next9Text.add("Next_" + weekdays[todayIx]);
        next9Text.add("Next_" + weekdays[todayPlus1Ix]);

        for (int i = 0; i < next9Text.size(); i++) {
            next9Date.add(this.today.add(Calendar.DATE, i).getDateTime());
            next9TextLowered.add(next9Text.get(i).toLowerCase());
        }
    }

    private static int toDaysIndex(int calendarDOW) {
        return (calendarDOW + 7 - Calendar.MONDAY) % 7;
    }

    public String toDowString(DateTime dateTime) {
        return toDowString(new Cal(dateTime));
    }

    public String toDowString(Cal cal) {
        return weekdays[toDaysIndex(cal.get(Calendar.DAY_OF_WEEK))];
    }

    public String toShortDowString(DateTime dateTime) {
        return toShortDowString(new Cal(dateTime));
    }

    public String toShortDowString(Cal cal) {
        return shortWeekdays[toDaysIndex(cal.get(Calendar.DAY_OF_WEEK))];
    }

    public DateTime fromNext9DaysString(String text) {
        int ix = next9TextLowered.indexOf(text.toLowerCase());
        return ix >= 0 ? next9Date.get(ix) : null;
    }

    public String toNext9DaysString(DateTime dateTime) {
        return toNext9DaysString(new Cal(dateTime));
    }

    public String toNext9DaysString(Cal cal) {
        int ix = next9Date.indexOf(cal.beginningOfDay().getDateTime());
        return ix >= 0 ? next9Text.get(ix) : null;
    }

    private String toShort(DateTime dateTime) {
        return dateTime.format(
                today.add(Calendar.DATE, -90).before(dateTime) &&
                        today.add(Calendar.DATE, 180).after(dateTime) ? shortDateFormat : dateFormat
        );
    }

    public String toShortDateString(DateTime dateTime) {
        String option = toNext9DaysString(dateTime);
        return option != null ? option : toShort(dateTime);
    }

    public String toLongDateString(DateTime dateTime) {
        String text = toNext9DaysString(dateTime);
        return text != null ?
                String.format("%s (%s)", text, dateTime.format(shortDateFormat)) :
                String.format("%s (%s)", toShort(dateTime), toShortDowString(dateTime));
    }

    public DateTime fromShortDateString(String text) {
        DateTime option = fromNext9DaysString(text);
        if (option != null) return option;

        // try a full parse
        try {
            return DateTime.parse(text, dateFormat);
        } catch (Exception e) {
            // ignore
        }

        // we might don't have the year in the string, so try to deduce it
        int thisYear = today.get(Calendar.YEAR);
        Cal cal = new Cal(DateTime.parse(text, shortDateFormat)).set(Calendar.YEAR, thisYear);
        if (today.add(Calendar.DATE, -90).after(cal)) return cal.set(Calendar.YEAR, thisYear + 1).getDateTime();
        if (today.add(Calendar.DATE, 180).before(cal)) return cal.set(Calendar.YEAR, thisYear - 1).getDateTime();
        return cal.getDateTime();
    }

    public String toLongReminderString(DateTime due, Integer reminder) {
        if (reminder == null) return null;

        Integer inMin = null;
        if (due != null) {
            DateTime popupTime = new Cal(due).add(Calendar.MINUTE, reminder).getDateTime();
            inMin = (int) ((popupTime.getTicks() - new DateTime().getTicks()) / (1000 * 60));
        }

        if (reminder < 0 && (inMin == null || inMin >= 60 || inMin < -60)) return String.format("%s min before", -reminder);
        if (reminder == 0 && (inMin == null || inMin >= 60 || inMin < -60)) return "on time";
        if (reminder > 0 && (inMin == null || inMin > reminder || inMin < -60)) return String.format("%s min after", reminder);

        if (inMin == null) throw new IllegalStateException();
        if (inMin < 0) return String.format("%s min ago", -inMin);
        return String.format("in %s min", inMin);
    }

    public String toShortReminderString(DateTime due, Integer reminder) {
        if (reminder == null) return null;

        Integer inMin = null;
        if (due != null) {
            DateTime popupTime = new Cal(due).add(Calendar.MINUTE, reminder).getDateTime();
            inMin = (int) ((popupTime.getTicks() - new DateTime().getTicks()) / (1000 * 60));
        }

        if (reminder < 0 && (inMin == null || inMin >= 60 || inMin < -60)) return String.format("-%s", -reminder);
        if (reminder == 0 && (inMin == null || inMin >= 60 || inMin < -60)) return "0";
        if (reminder > 0 && (inMin == null || inMin > reminder || inMin < -60)) return String.format("-%s", reminder);

        if (inMin == null) throw new IllegalStateException();
        if (inMin < 0) return String.format("--%s", -inMin);
        return String.format("++%s", inMin);
    }

    public Integer fromReminderString(String text, DateTime due) {
        if (StringUtils.isEmpty(text)) return null;

        text = text.toLowerCase();

        if (PATTERN_ON_TIME.matcher(text).matches()) return 0;

        try {
            Matcher matcher = PATTERN_MIN_BEFORE.matcher(text);
            if (matcher.matches()) {
                return -Integer.parseInt(matcher.group(1) != null ? matcher.group(1) : matcher.group(2));
            }

            matcher = PATTERN_MIN_AFTER.matcher(text);
            if (matcher.matches()) {
                return Integer.parseInt(matcher.group(1) != null ? matcher.group(1) : matcher.group(2));
            }

            matcher = PATTERN_MIN_AGO.matcher(text);
            if (matcher.matches()) {
                String match = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
                DateTime popupTime = new Cal().add(Calendar.MINUTE, -Integer.parseInt(match)).getDateTime();
                return (int) ((popupTime.getTicks() - due.getTicks()) / (1000 * 60)) + 1;
            }

            matcher = PATTERN_IN_MIN.matcher(text);
            if (matcher.matches()) {
                String match = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
                DateTime popupTime = new Cal().add(Calendar.MINUTE, Integer.parseInt(match)).getDateTime();
                return (int) ((popupTime.getTicks() - due.getTicks()) / (1000 * 60)) + 1;
            }
        }
        catch (NumberFormatException e) {
            // ignore
        }
        throw new IllegalFormattingException();
    }

}
